<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
  <style>
    .img {
      width: 500px;
      height: auto;
    }
  </style>
</head>
<body>
  <script>
    let tmpl = `<div class="newslist" id="se">
                  <div class="img" v-if="info.showImage"><img class="img" src="{{image}}"/></div>
                  <div class="date" v-if="info.showDate">{{info.name}}></div>
                  <div class="img">{{info.name}} - {{info.age}}></div>
                </div>`;
 

    render(tmpl, {
      image: "https://pic2.zhimg.com/v2-95e64e2c208f50ebb7cfa36399b53703_1440w.jpg?source=172ae18b", 
      info: {showImage: true, showDate:false, name: "aaa", age: 26}
    })

    function render(tmpl, data) {
      const nodeTree = tmplToNodeTree(tmpl)
      const domTree = nodeTreeToDomTree(nodeTree, data)
      document.body.appendChild(domTree)
    }
   
    // 将tmpl装换成nodeTree
    // 从头到尾顺序进循环匹配，通过维护一个node栈来确保正确的node层级
    function tmplToNodeTree(tmpl) {
      const nodeStack = []
      const topNode = {
        tag: '',
        children: []
      }
      
      let curNode = null
      let parentNode = topNode
      tmpl = tmpl.trim()
      let count = 0  // 加个限制，循环超过这个限制，多半是死循环了，（需要退出并抛出错误）
      while(/<([^>]+)>/.test(tmpl) && count++ < 10000) {
        let curMatchData = ''
        // 如果是以标签开始
        if(/^\s*<([^>]+)>/.test(tmpl)) {
          let closeFlag = null
          // 开始标签，或者自闭和标签
          if(/^\s*<([^>\/]+)(\/)?(\s*)>/.test(tmpl)) {
            tmpl = tmpl.replace(/^\s*<([^>\/]+)(\/)?(\s*)>/, function(match, $1, $2) {
              closeFlag = !!$2
              curMatchData = $1.trim();
              return ''
            }).trim();

            curMatchData = curMatchData.split(' ');
            curNode = createVNode(curMatchData)
            parentNode.children.push(curNode)
            // 非自闭和标签， 压栈，更换当前父节点，下一次循环开始解析当前节点的子节点
            if(!closeFlag) { 
              nodeStack.push(curNode)
              parentNode = curNode
            }
            
           // 闭合标签
          } else {
            tmpl = tmpl.replace(/^\s*<([^>]+)>/, function(match, $1) {
              closeFlag = true
              curMatchData = $1.trim();
              return ''
            }).trim();
            // 出栈，更换当前父节点，下一次循环开始解析当前节点的兄弟节点
            nodeStack.pop()
            parentNode = nodeStack[nodeStack.length - 1]
          }

          // 内容部分
        } else {
          tmpl = tmpl.replace(/([\w\W]+?)(?=<[^<>]+>)/, function(match, $1) {
            console.log($1);
            curMatchData = $1.trim();
            return ''
          }).trim();
          // 按照文本节点处理
          parentNode.children.push(curMatchData)
        }     
      }
      
      return topNode
    }
    
    // 创建VNode
    function createVNode(curMatchData) {
      const props = {}
      const directives = {}
        for (let i = 1; i < curMatchData.length; i++) {
          const propMatchData = curMatchData[i].trim().match(/([\w\W]+)="([\w\W]+)"/);
          if(propMatchData) {
            const [, key, value] = propMatchData
            if(/^v-/.test(key)) {
              directives[key.substr(2)] = value
            } else {
              props[key] = value
            }        
          }
        }
        const VNode = {
          tag: curMatchData[0].trim(),
          props,
          directives,
          children: []
        }
        return VNode
    }
    
    // 由nodeTree数据创建domTree,并将a.b.c之类的替换成实际的值
    function nodeTreeToDomTree(nodeTree, data) {
      let curDom = null
      const {tag, children = [], props = {}, directives = {}} = nodeTree

      const curIfValuePath = directives.if
      if(curIfValuePath && !getValueByKeyPath(data, directives.if)) {
        return document.createTextNode('')
      }
      if(!tag) {
        curDom = document.createDocumentFragment()
      } else if(tag === 'img'){
        curDom = new Image()
        for (const prop in props) {
          curDom.setAttribute(prop, getValue(data, props[prop]))
        }
      } else {
        curDom = document.createElement(tag)
        for (const prop in props) {
          curDom.setAttribute(prop, getValue(data, props[prop]))
        }
      }
      if(children && !!children.length) {
        for (const item of children) {
          if(typeof item === 'string') {
            curDom.appendChild(document.createTextNode(getValue(data, item)))
          } else {
            curDom.appendChild(nodeTreeToDomTree(item, data))
          }         
        }
        
      }    
      return curDom
      // 校验字符串是带Mustache模板还是普通字符串，如果是Mustache,就将替换{{a.b}} 通过合理替换拼接keyPath('a.b.c'), 也可以正确取到v-for内的item.key
      function getValue(data, key) {
        let value = key
        if(/\{\{([^\}]+)\}\}/.test(key)) {
          const path = key.match(/\{\{([^\}]+)\}\}/g)[1]
          value = key.replace(/\{\{([^\}]+)\}\}/g, function(match, $1) {
            return getValueByKeyPath(data, $1)
          })    
        }
        return value
      }
    }
    
    // 通过keyPath(a.b.c)取值
    function getValueByKeyPath(data, path = '') {
      if(!path) return ''
      pathArr = path.split('.')
      let value = data
      for (const item of pathArr) {
        // if(typeof value === 'object') {
        //   value = value[item] 
        // }
        // 不做校验，该报错直接报错
        value = value[item] 
      }
      return value
    }
  </script>
</body>
</html>